package com.example.demo.jwt;

import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.demo.redis.RedisClient;
import com.example.demo.result.JsonResult;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author zhanpengguo
 * @date 2019-07-04 11:20
 */
public class JWTFilter extends BasicHttpAuthenticationFilter {

    private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);

    @Autowired
    RedisClient redisClient;
    @Value("${refreshTokenExpireTime}")
    String refreshTokenExpireTime;

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        logger.debug("=========校验权限===========");
        if (isLoginAttempt(request,response)){
            try {
                executeLogin(request,response);
            }catch (Exception e){
                //认证出现异常
                String msg = e.getMessage();
                logger.warn("认证出现异常:{}",msg);
                Throwable throwable = e.getCause();
                if (throwable != null && throwable instanceof SignatureVerificationException){
                    // 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
                    msg = "token或者密钥不正确(" + throwable.getMessage() + ")";
                }else if (throwable != null && throwable instanceof TokenExpiredException){
                    // 该异常为JWT的AccessToken已过期，判断RefreshToken未过期就进行AccessToken刷新
                    if (this.refreshToken(request,response)){
                        return true;
                    }else {
                        msg = "token已过期:"+throwable.getMessage();
                    }
                }else {
                    if (throwable != null){
                        msg = throwable.getMessage();
                    }
                }
                this.response401(request,response,msg);
                return false;
            }
        }
        return true;
    }

    /**
     * 校验是否登陆，查看请求头中是否有token
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        logger.debug("=========获取请求头信息===========");
        String token = this.getAuthzHeader(request);
        //如果有token，继续走executeLogin逻辑
        return token != null;
    }
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        this.sendChallenge(request, response);
        return false;
    }
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        logger.debug("=========校验登陆===========");
        String token = this.getAuthzHeader(request);
        JWTToken jwtToken = new JWTToken(token);
        //交给自定义realm进行认证，如果错误则抛出异常并捕获，
        getSubject(request,response).login(jwtToken);
        //如果没有抛出异常则表示登入成功
        return true;
    }

    private boolean refreshToken(ServletRequest request, ServletResponse response){
        logger.debug("=========刷新token===========");
        String token = getAuthzHeader(request);
        String username = JWTUtil.getUsername(token);
        if (redisClient.hasKey(JWTConstant.getRefreshTokenKey(username))){
            //redis中refreshtoken还存在，获取时间戳
            String currentTimeMillisRedis = redisClient.get(JWTConstant.getRefreshTokenKey(username)).toString();
            //获取token中的时间戳
            String currentTimeMillisToken = JWTUtil.getClaim(token, JWTConstant.CLAIM_CURRENT);
            if (currentTimeMillisRedis.equals(currentTimeMillisToken)){
                //获取当前时间戳
                String currentMillis = String.valueOf(System.currentTimeMillis());
                redisClient.set(JWTConstant.getRefreshTokenKey(username),currentMillis,Integer.parseInt(refreshTokenExpireTime));
                //刷新token，设置时间戳为当前时间戳
                token = JWTUtil.sign(username,currentMillis);
                //将刷新的token再次进行shiro登陆
                JWTToken jwtToken = new JWTToken(token);
                this.getSubject(request,response).login(jwtToken);
                //最后将刷新的token放入header中
                HttpServletResponse resp = (HttpServletResponse) response;
                resp.setHeader("Authorization",token);
                resp.setHeader("Access-Control-Expose-Headers", "Authorization");
                return true;
            }
            logger.warn("redis中refreshtoken和当前token中时间戳不一致，不能刷新token");
            return false;
        }
        logger.warn("redis中没有此refreshtoken");
        return false;
    }

    /**
     * 无需转发，直接返回Response信息
     */
    private void response401(ServletRequest req, ServletResponse resp, String msg) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
        httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        try (PrintWriter out = httpServletResponse.getWriter()){
            String data = new JsonResult("401","无权访问(Unauthorized)",msg).toString();
            out.append(data);
        } catch (IOException e) {
            logger.error("直接返回Response信息出现IOException异常:" + e.getMessage());
            throw new RuntimeException("直接返回Response信息出现IOException异常:" + e.getMessage());
        }
    }

    /**
     * 对跨域请求支持
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        logger.debug("=========设置跨域===========");
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setHeader("Access-control-Allow-Origin", req.getHeader("Origin"));
        resp.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        resp.setHeader("Access-Control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())){
            resp.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}
